﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Text.RegularExpressions;
using System.Web.UI.WebControls;
using Ext.Net.UIS.Script;
using KamLib;
using KamLib.Converts;


namespace Ext.Net.UIS
{

    public partial class Term
    {

        #region Types

        public class List : ObservableCollection<Term>
        {
            public new void Add(Term item)
            {
                if (item != null)
                    base.Add(item);
            }

            public void AddRange(params Term[] items)
            {
                foreach (var item in items)
                {
                    if (item != null)
                        base.Add(item);
                }
            }
        }
        public class Dictionary : SortedDictionary<string, Term> { }

        public class DebugClass
        {
            private readonly Term Term;
            public DebugClass(Term term) { Term = term; }

            public DebugClass[] Items { get { return Term.Items.Select(a => a.__Debug).ToArray(); } }
            public TermTitle title { get { return Term.Title; } }
            public override string ToString() { return Term.ToString(); }

        }

        public DebugClass __Debug { get { return new DebugClass(this); } }

        #endregion


        #region Register

        public readonly static Dictionary Objects = new Dictionary();

        public static void Register(Term term)
        {
            if (term == null) throw new ArgumentNullException("term");
            Objects[term.FullName] = term;
            Register(term.Items);
        }

        public static void Register(IEnumerable<Term> terms)
        {
            foreach (var term in terms)
            {
                Register(term);
            }
        }

        #endregion


        #region Get, Parse

        public static Term Get(string name, TermNode parentNode)
        {
            while (parentNode != null && parentNode.Term == null)
            {
                parentNode = parentNode.Parent;
            }
            return Get(name, parentNode != null ? parentNode.Term : null);
        }

        public static Term Get(string name, Term parent = null)
        {
            if (name.no())
                throw new ArgumentNullException("name");

            if (name.IndexOfAny(ParseChars) >= 0)
            {
                var term = new Term(name) { Parent = parent, IsAnonymous = true };
                term.ParseAdd(name, parent);
                return term;
            }

            var dic = parent != null ? parent.AllItems : Objects;

            Term o = null;
            var name0 = name;
            string name1 = null;

            // Поиск от конца name к корню хоть какой-то части в parent
            // Это на случай, если в основном справочнике полей не все item-terms зарегистрированы.
            // Например, зарегистрирована только таблица, но не её поля
            while (name0 != null && !dic.TryGetValue(name0, out o))
            {
                var i = name0.LastIndexOf('\\');
                if (i <= 0)
                {
                    name0 = null;
                    name1 = name;
                }
                else
                {
                    name0 = name0.Left(i);
                    name1 = name.Right(name.Length - i - 1);
                }
            }

            // Если какая-то часть найдена, и это не поле целиком,
            // то в ней поискать недостающий кусочек
            if (o != null && name1 != null)
                o = Get(name1, o);

            // Если ничего не найдено, то поискать уровнем выше
            if (o == null && parent != null)
                o = Get(name, parent.Parent);

            if (o == null)
                throw new Exception(
                    "UIS.Term.Get: Can't find term '" + name + "'" +
                    (parent != null ? " in term '" + parent.FullName + "'" : null)
                );

            return o;
        }

        private static readonly char[] ParseChars = new[] { ';', ',', '+' };

        /// <summary>
        /// Для отлова labels 'label1'
        /// </summary>
        static readonly Regex re1 = new Regex(@"(?<!\\)'(.*?)(?<!\\)'");
        /// <summary>
        /// вспомогательный для labels
        /// </summary>
        static readonly Regex re2 = new Regex(@"\{\{\{(\d+)\}\}\}");
        /// <summary>
        /// для отлова titles {new title}
        /// </summary>
        static readonly Regex re3 = new Regex(@"\{(.*?)\}");


        /// <summary>
        /// Добавляет Items, на основании fieldNames
        /// </summary>
        /// <param name="parentField"> </param>
        /// <param name="fieldNames">в формате:
        /// field1, term2 /* это будет в одну строчку */; field3; field4;
        /// </param>
        public void ParseAdd(string fieldNames, Term parentField)
        {
            if (fieldNames.no()) return;

            #region Поиск labels и подмена их для дальнейшего парсинга

            var s = fieldNames;
            var i = 0;
            var labels = new List<string>();
            var m = re1.Match(s);
            while (m.Success)
            {
                s = s.Left(m.Index) + "{{{" + (i++) + "}}}" + s.Substring(m.Index + m.Length);
                labels.Add(m.Groups[1].Value.Replace(@"\'", "'"));
                m = re1.Match(s);
            }

            #endregion

            var isRow = false;
            var row = this;

            Func<string, Term> get = name =>
            {
                name = name.Trim();
                if (name.no()) return null;

                // Labels
                m = re2.Match(name);
                if (m.Success)
                {
                    var i2 = m.Groups[1].Value.ToInt();
                    name = name.Left(m.Index) + name.Substring(m.Index + m.Length);
                    if (isRow && (m = re3.Match(name)).Success)
                        row.Title = m.Groups[1].Value;
                    return new Label(labels[i2]);
                }

                // Titles
                m = re3.Match(name);
                if (m.Success)
                {
                    name = name.Left(m.Index) + name.Substring(m.Index + m.Length);
                    var term = Get(name.Trim(), parentField);
                    if (isRow)
                        row.Title = m.Groups[1].Value;
                    return term;
                }

                return Get(name.Trim(), parentField);
            };


            Action<string[], string> parseUnions = (parseNames, parseName) =>
            {
                var names = parseName.Split('+');

                // одно поле в строке
                if (names.Length == 1)
                {
                    var item = get(parseName);
                    row.Items.Add(item);
                }
                // если текущий term является Union (FieldContainer), либо его можно сделать таковым
                else if (isRow || parseNames.Length == 1)
                {
                    if (parseNames.Length == 1)
                    {
                        MakeUnion();
                        isRow = true;
                        row = this;
                    }
                    foreach (var name2 in names)
                    {
                        var item = get(name2);
                        row.Items.Add(item);
                    }
                }
                // создать новую строчку, и туда уже пихать
                else
                {
                    row = new Term();
                    row.MakeUnion();
                    isRow = true;
                    foreach (var name2 in names)
                    {
                        var item = get(name2);
                        row.Items.Add(item);
                    }
                    Items.Add(row);
                }
            };


            Action<string[], string> parseRows = (parseNames, parseName) =>
            {
                var names = parseName.Split(',');

                // одно поле в строке
                if (names.Length == 1)
                {
                    parseUnions(parseNames, parseName);
                }
                // если текущий term является Union (FieldContainer), либо его можно сделать таковым
                else if (isRow || parseNames.Length == 1)
                {
                    if (parseNames.Length == 1)
                    {
                        MakeRow();
                        isRow = true;
                    }
                    foreach (var name2 in names)
                    {
                        parseUnions(names, name2);
                    }
                }
                // создать новую строчку, и туда уже пихать
                else
                {
                    row = new Term();
                    row.MakeRow();
                    isRow = true;
                    foreach (var name2 in names)
                    {
                        parseUnions(names, name2);
                    }
                    Items.Add(row);
                }
            };

            #region Поиск строк

            var names1 = s.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
            foreach (var name1 in names1)
            {
                isRow = IsRow() || IsUnion();
                row = this;
                parseRows(names1, name1);
            }

            #endregion
        }

        #endregion


        #region Conctructors

        public Term() { }


        public Term(string name)
            : this()
        {
            Name = name;
            Title = name;
        }
        public Term(string name, params TermTag[] tags)
            : this(name)
        {
            Add(tags);
        }
        public Term(string name, params Term[] items)
            : this(name)
        {
            Items.AddRange(items);
        }

        public Term(string name, TermTitle title)
            : this(name)
        {
            Title = title ?? name;
        }
        public Term(string name, TermTitle title, params TermTag[] tags)
            : this(name, title)
        {
            Add(tags);
        }
        public Term(string name, TermTitle title, params Term[] items)
            : this(name, title)
        {
            Items.AddRange(items);
        }


        public Term(string name, Type dataType)
            : this(name)
        {
            DataType = dataType;
        }
        public Term(string name, Type dataType, params TermTag[] tags)
            : this(name, tags)
        {
            DataType = dataType;
        }
        public Term(string name, Type dataType, params Term[] items)
            : this(name, items)
        {
            DataType = dataType;
        }

        public Term(string name, Type dataType, TermTitle title)
            : this(name, title)
        {
            DataType = dataType;
        }
        public Term(string name, Type dataType, TermTitle title, params TermTag[] tags)
            : this(name, title, tags)
        {
            DataType = dataType;
        }
        public Term(string name, Type dataType, TermTitle title, params Term[] items)
            : this(name, title, items)
        {
            DataType = dataType;
        }


        #endregion


        #region Base

        public string Name { get { return _Name; } set { _Name = value; _FullName = null; } } string _Name;
        public Type DataType { get; set; }
        /// <summary>
        /// Если true, то этот term включать в Path для всех items  
        /// </summary>
        public bool IsNamespace { get; set; }
        public string EditorName { get; set; }

        /// <summary>
        /// Показывает, формировалось ли данное поле как составное через компоненты интерфейса
        /// </summary>
        public bool IsAnonymous { get; protected set; }

        /// <summary>
        /// Полное имя, по которому искать через Term.Get.
        /// </summary>
        public string FullName { get { return _FullName ?? (_FullName = ParentPath + Name); } } string _FullName;

        /// <summary>
        /// Полное имя, по которому искать .
        /// </summary>
        public string FieldName
        {
            get
            {
                if (_FieldName == null)
                {
                    if (Items.Count > 0) return _FieldName = "";

                    _FieldName = Name;
                    var p = Parent;
                    while (p != null && !p.IsEntity())
                    {
                        if (p.IsNamespace && p.Name.yes())
                        {
                            _FieldName = p.Name + '.' + _FieldName;
                        }
                        p = p.Parent;
                    }
                }

                return _FieldName;
            }
        } string _FieldName;

        public override string ToString()
        {
            var sb = new StringBuilder();
            sb.Append("Term " + FullName);
            if (Tags.Count > 0)
            {
                sb.Append(" (");
                foreach (var tag in Tags)
                {
                    sb.Append(tag.Name);
                    sb.Append(", ");
                }
                sb.Length -= 2;
                sb.Append(")");
            }

            return sb.ToString();
        }

        #endregion


        #region Title

        public TermTitle Title { get; set; }

        public TermTitle GetTitle(TermNode parentNode)
        {
            if (Title == null) return null;

            Title.PrepareNamespaces(this);

            var p = parentNode;
            while (p != null)
            {
                TermTitle t;
                if (p.Term != null && !p.Term.IsAnonymous && p.Term.Name.yes() &&
                    Title.Namespaces.TryGetValue(p.Term.Name, out t))
                    return t;
                p = p.Parent;
            }

            return Title;
        }

        public Term SetTitle(string middle, params string[] ns)
        {
            Title = new TermTitle(middle, ns);
            return this;
        }

        public Term SetTitle(string middle, object ns)
        {
            Title = new TermTitle(middle, ns);
            return this;
        }

        #endregion


        #region Control Properties

        public int Flex { get { return _Flex; } set { _Flex = value; } } int _Flex = 1;
        public Unit Width { get; set; }
        public int Length { get; set; }


        #endregion


        #region Parent, Items

        /// <summary>
        /// Первичный Parent, по которому определяется уникальное имя поля
        /// </summary>
        public Term Parent { get; set; }
        /// <summary>
        /// Путь к данному term (без учёта собственного Name).
        /// </summary>
        public string ParentPath
        {
            get
            {
                if (_ParentPath != null) return _ParentPath;
                _ParentPath = null;
                var p = Parent;
                while (p != null)
                {
                    if (p.IsNamespace && p.Name.yes())
                    {
                        _ParentPath = p.ParentPath + p.Name + '\\';
                        break;
                    }
                    p = p.Parent;
                }

                return _ParentPath;
            }
        } string _ParentPath;

        public string ItemNames { get; set; }
        protected List _Items;
        public List Items
        {
            get
            {
                if (_Items == null)
                {
                    _Items = new List();
                    _Items.CollectionChanged += ItemsChanged;
                }

                if (ItemNames != null)
                {
                    var itemNames = ItemNames;
                    ItemNames = null;
                    ParseAdd(itemNames, this);
                }
                return _Items;
            }
        }
        /// <summary>
        /// Хранит справочник всего вложенного дерева Items. 
        /// Ключом является краткий путь от parent до item (как имя свойства объекта), 
        /// типа: ID, Name, Phone.Mobile.
        /// </summary>
        public readonly Dictionary AllItems = new Dictionary();


        protected void ItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    foreach (Term item in e.NewItems)
                    {
                        if (item.Parent != null) continue;
                        item.Parent = this;
                        item.RegisterInParents();
                    }
                    break;
                case NotifyCollectionChangedAction.Remove:
                    foreach (Term item in e.OldItems)
                    {
                        if (item.Parent != this) continue;
                        item.RegisterInParents(true);
                        item.Parent = null;
                    }
                    break;
            }
        }

        protected void RegisterInParents(bool remove = false)
        {
            if (Name.no()) return;

            var p = Parent;
            var name = Name;
            while (p != null)
            {
                if (remove)
                    p.AllItems.Remove(name);
                else
                    p.AllItems[name] = this;

                if (p.IsNamespace && p.Parent != null)
                    name = p.Name + '\\' + name;
                p = p.Parent;
            }

            foreach (var item in Items)
            {
                item.RegisterInParents(remove);
            }
        }


        public Term AddItems(params Term[] items)
        {
            Items.AddRange(items);
            return this;
        }


        #endregion


        #region Find

        public Term FindIdItem() { return FindDown(a => a.IsEntity() ? -1 : a.Have(TermTags.ID) ? 1 : 0); }
        public Term FindNameItem() { return FindDown(a => a.IsEntity() ? -1 : a.Have(TermTags.Name) ? 1 : 0); }


        public Term FindItem(Func<Term, bool> match)
        {
            if (match == null)
                throw new ArgumentNullException("match");

            foreach (var item in Items)
            {
                if (match(item)) return item;
            }

            return null;
        }

        public Term FindSibling(Func<Term, bool> match)
        {
            return Parent != null ? Parent.FindItem(match) : null;
        }

        public Term FindUp(Func<Term, bool> match)
        {
            var term = FindSibling(match);
            if (term == null && Parent != null)
                term = Parent.FindUp(match);
            return term;
        }

        public Term FindDown(Func<Term, bool> match)
        {
            var term = FindItem(match);
            if (term == null)
            {
                foreach (var item in Items)
                {
                    term = item.FindDown(match);
                    if (term != null) break;
                }
            }

            return term;
        }


        public Term FindItem(Func<Term, int> match)
        {
            if (match == null)
                throw new ArgumentNullException("match");

            foreach (var item in Items)
            {
                var result = match(item);
                if (result == 1) return item;
                if (result < 0) return null;
            }

            return null;
        }

        public Term FindSibling(Func<Term, int> match)
        {
            return Parent != null ? Parent.FindItem(match) : null;
        }

        public Term FindUp(Func<Term, int> match)
        {
            var term = FindSibling(match);
            if (term == null && Parent != null)
                term = Parent.FindUp(match);
            return term;
        }

        public Term FindDown(Func<Term, int> match)
        {
            var term = FindItem(match);
            if (term == null)
            {
                foreach (var item in Items)
                {
                    term = item.FindDown(match);
                    if (term != null) break;
                }
            }

            return term;
        }

        #endregion


        #region Tags

        protected internal readonly TermTag.List Tags = new TermTag.List();

        public void Add(TermTag tag)
        {
            tag.AddTo(this);

            if (!IsNamespace && tag.IsNamespace)
            {
                IsNamespace = true;
            }
        }

        public void Add(params TermTag[] tags)
        {
            foreach (var tag in tags)
            {
                Add(tag);
            }
        }
        public void Add(IEnumerable<TermTag> tags)
        {
            foreach (var tag in tags)
            {
                Add(tag);
            }
        }

        public bool Have(TermTag tag)
        {
            return tag.IsAddedTo(this);
        }

        public bool ParentHave(TermTag tag)
        {
            return Parent != null && Parent.Have(tag);
        }

        #endregion


        #region Lookup

        public string LookupEntityName { get; set; }

        public string LookupKeyFieldName
        {
            get
            {
                return _lookupKeyFieldName ??
                    (LookupKeyField != null ? _lookupKeyFieldName = _lookupKeyField.Name : null);
            }
            set { _lookupKeyFieldName = value; }
        } string _lookupKeyFieldName;

        public string LookupNameFieldName
        {
            get
            {
                return _lookupNameFieldName ??
                    (LookupNameField != null ? _lookupNameFieldName = _lookupNameField.Name : null);
            }
            set { _lookupNameFieldName = value; }
        } string _lookupNameFieldName;


        public Term LookupEntity
        {
            get
            {
                if (_lookupEntity == null && LookupEntityName.yes())
                    _lookupEntity = Get(LookupEntityName, this);
                return _lookupEntity;
            }
            set
            {
                _lookupEntity = value;
                if (value != null)
                    LookupEntityName = value.Name;
            }
        } Term _lookupEntity;


        public Term LookupKeyField
        {
            get
            {
                if (_lookupKeyField == null)
                {
                    _lookupKeyField = _lookupKeyFieldName.yes()
                         ? Get(_lookupKeyFieldName, LookupEntity)
                         : LookupEntity.FindIdItem();
                    if (_lookupKeyField == null)
                        throw new Exception("UIS.Term.SetLookup: can't find ID-term in entity '" + LookupEntity.Name + "'");
                }
                return _lookupKeyField;
            }
            set
            {
                _lookupKeyField = value;
                if (value != null)
                    _lookupKeyFieldName = value.Name;
            }
        } Term _lookupKeyField;


        public Term LookupNameField
        {
            get
            {
                if (_lookupNameField == null)
                {
                    _lookupNameField = _lookupNameFieldName.yes()
                        ? Get(_lookupNameFieldName, LookupEntity)
                        : LookupEntity.FindNameItem();
                    if (_lookupNameField == null)
                        throw new Exception("UIS.Term.SetLookup: can't find Name-term in entity '" + LookupEntity.Name + "'");
                }
                return _lookupNameField;
            }
            set
            {
                _lookupNameField = value;
                if (value != null)
                    _lookupNameFieldName = value.Name;
            }
        } Term _lookupNameField;


        public Term SetLookup(string lookupEntity, string lookupKeyField = null, string lookupListField = null)
        {
            LookupEntityName = lookupEntity;
            LookupKeyFieldName = lookupKeyField;
            LookupNameFieldName = lookupKeyField;
            return this;
        }

        public Term SetLookup(Term lookupEntity, string lookupKeyField = null, string lookupListField = null)
        {
            LookupEntity = lookupEntity;
            LookupKeyFieldName = lookupKeyField;
            LookupNameFieldName = lookupKeyField;
            return this;
        }

        public Term SetLookup(Term lookupEntity, Term lookupKeyField = null, Term lookupListField = null)
        {
            LookupEntity = lookupEntity;
            LookupKeyField = lookupKeyField;
            LookupNameField = lookupKeyField;
            return this;
        }


        #endregion


        #region Events

        public TermScript ChangeScript { get; set; }
        public Term OnChange(params Expression[] expressions)
        {
            if (expressions != null && expressions.Length > 0)
            {
                ChangeScript = new TermScript(Expression.Lambda(
                    Expression.Block(typeof(void), expressions), new[] { TermScript.Field, TermScript.Value }
                ));
            }
            return this;
        }

        public TermScript CalculateScript { get; set; }
        public Term OnCalculate(Expression<Func<object>> formula)
        {
            if (formula != null)
            {
                CalculateScript = new TermScript(Expression.Lambda(
                    formula.Body, new[] { TermScript.Value, TermScript.Record }
                ));
            }
            return this;
        }

        #endregion

    }

}
